16 缺失值-英国零售商数据处理
16.1 引言数据质量的核心挑战
在真实的数据分析项目中,缺失值(Missing Values)是数据质量问题中最常见也最棘手的挑战之一。对于金融分析师而言,正确处理缺失值直接关系到分析结果的可靠性和投资决策的有效性。
16.2 本章学习目标
通过本章学习,你将能够:
- 使用
isnull/isna、.sum()、.mean()完成缺失值的定位、计数与比例诊断 - 说出 MCAR、MAR、MNAR 三种缺失机制的含义及其对处理策略的约束
- 根据数据类型与业务逻辑选择删除(
dropna的how/subset/axis)或填充(fillna、ffill/bfill、interpolate)策略 - 在时间序列与面板数据中选择合适的填充方式,并解释前向填充、线性插值的假设与风险
- 按平台任务要求完成英国零售商数据中
CustomerID缺失行的剔除
16.3 缺失值识别与诊断
16.3.1 基础检测方法
平台任务(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:从平台内置的 英国零售商data.csv 读取交易数据(CustomerID、InvoiceID 两列强制按字符串类型读入),剔除 CustomerID 为缺失值的行,输出该列剩余的非缺失值计数。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:英国零售商data.csv数据文件本地没有,但平台已经内置
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
path= '英国零售商data.csv' # 设置path为"英国零售商data.csv"
df=pd.read_csv(path,dtype={'CustomerID':str,'InvoiceID':str}) # 从CSV文件读取数据存入df
# 剔除CustomerID的缺失数据
df.dropna(subset=['CustomerID'],inplace=True)
print(df.CustomerID.count()) # 输出非缺失值计数预期输出(数据文件为教学平台内置,本机无法复现,判读要点如下;具体数值以平台运行结果为准):
- 输出为一个整数,等于剔除
CustomerID缺失行后该列的非缺失计数,应明显小于原始总行数(该数据集中未登录客户的交易记录占比较高) dtype={'CustomerID': str}保证客户编号按文本读入,避免被误读为数值后丢失格式信息dropna(subset=['CustomerID'])只按指定列剔除整行,其他列的缺失不影响行去留;inplace=True表示直接在df上修改
# =============================================================================
# 题目:缺失值检测与统计
# =============================================================================
# 本任务演示如何在实际商业数据中识别和统计缺失值
# 场景:英国零售商连锁店的销售数据,部分门店数据缺失
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库,用于处理表格数据
import numpy as np # NumPy数值计算库,用于创建NaN缺失值
# ==================== 创建包含缺失值的示例数据 ====================
# 场景:6家零售门店(A-F)的日销售数据
data = {
# 门店代码列:6家门店的标识符
'Store': ['A', 'B', 'C', 'D', 'E', 'F'],
# 销售额列:门店B和E的销售额缺失(可能是系统故障或未营业)
'Sales': [100, np.nan, 150, 200, np.nan, 180],
# 利润列:门店C和F的利润缺失(可能是成本数据未录入)
'Profit': [10, 15, np.nan, 20, 25, np.nan],
# 客户数列:门店D的客户数缺失(可能是客流计数器故障)
'Customers': [50, 75, 100, np.nan, 80, 95]
}
# 将字典转换为Pandas数据框(二维表格结构)
df = pd.DataFrame(data)
# ==================== 显示原始数据 ====================
print('原始数据:')
print(df) # 打印完整数据框,NaN会显示为NaN
print()
# ==================== 检测方法1:识别缺失值位置 ====================
print('缺失值位置(True表示缺失):')
# df.isnull():检测每个单元格是否为缺失值
# 返回布尔型数据框,True表示该位置是缺失值,False表示不是缺失值
# isnull()和isna()功能完全相同,可以互换使用
print(df.isnull())
# 输出示例:
# Store Sales Profit Customers
# 0 False False False False
# 1 False True False False <- 门店B的销售额缺失
# 2 False False True False <- 门店C的利润缺失
print()
# ==================== 检测方法2:统计每列缺失值数量 ====================
print('各列缺失值数量:')
# df.isnull().sum():统计每列的缺失值总数
# isnull()返回布尔值(True=1, False=0),sum()将True值相加
# 这是评估数据质量的关键指标:哪些变量问题最严重
print(df.isnull().sum())
# 输出解读:Sales列有2个缺失,Profit列有2个缺失,Customers列有1个缺失
print()
# ==================== 检测方法3:计算缺失值比例 ====================
print('各列缺失值比例:')
# df.isnull().mean() * 100:计算每列缺失值占总行数的百分比
# isnull().mean():先计算布尔值的均值(即True的比例)
# * 100:转换为百分比形式
# 缺失比例是决策依据:<5%可简单处理,>20%需考虑删除变量
print(df.isnull().mean() * 100)
# 输出解读:Sales缺失33.3%(2/6),Profit缺失33.3%,Customers缺失16.7%
print()
# ==================== 检测方法4:统计完整行数 ====================
print('完整行数(无缺失):', df.dropna().shape[0])
# df.dropna():删除包含任何缺失值的行,返回无缺失的数据框
# .shape[0]:获取数据框的行数(第一维度)
# 完整行数反映数据质量:完整行越少,数据质量问题越严重
print('总行数:', df.shape[0])
# shape属性返回(行数, 列数)的元组,shape[0]是行数,shape[1]是列数代码深度解析:
isnull()方法:- 返回布尔型DataFrame,缺失值位置为True
- 等价方法:
isna() - 适用于所有数据类型
- 统计策略:
- 按列统计: 了解哪些变量问题严重
- 按行统计: 识别数据质量最差的观测
- 比例计算: 评估数据质量的可接受程度
- 决策阈值:
- 缺失率 < 5%: 通常可删除或简单填充
- 缺失率 5%-20%: 需要谨慎选择策略
- 缺失率 > 20%: 考虑删除变量或使用高级插补
16.3.2 缺失模式可视化
# =============================================================================
# 题目:缺失模式可视化分析
# =============================================================================
# 本任务通过可视化手段展示缺失值的分布模式,帮助判断缺失机制
# 可视化比数字更直观,能快速发现数据质量问题
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
import matplotlib.pyplot as plt # Matplotlib绘图库
# ==================== 创建更大的示例数据集 ====================
# 设置随机种子,确保每次运行结果一致
np.random.seed(42)
# 创建100行数据,模拟100个交易日的数据
n_rows = 100
# 创建包含4个变量(A、B、C、D)的数据框
data_vis = pd.DataFrame({
'A': np.random.rand(n_rows), # 变量A:100个[0,1)区间的随机数
'B': np.random.rand(n_rows), # 变量B:100个[0,1)区间的随机数
'C': np.random.rand(n_rows), # 变量C:100个[0,1)区间的随机数
'D': np.random.rand(n_rows) # 变量D:100个[0,1)区间的随机数
})
# ==================== 人为引入缺失值(模拟真实场景) ====================
# 场景1:变量A的10%随机缺失(MCAR:完全随机缺失)
# np.random.rand(n_rows)生成100个[0,1)随机数
# < 0.1表示随机选择10%的位置
mask_a = np.random.rand(n_rows) < 0.1
# 将选中的位置设置为NaN(缺失值)
data_vis.loc[mask_a, 'A'] = np.nan
# 场景2:变量B的条件缺失(MAR:随机缺失)
# 当变量A的值<0.5时,变量B缺失
# 这模拟了相关性缺失:A值低时,B数据无法获取
data_vis.loc[data_vis['A'] < 0.5, 'B'] = np.nan
# 场景3:变量C的15%随机缺失
mask_c = np.random.rand(n_rows) < 0.15
data_vis.loc[mask_c, 'C'] = np.nan
# 场景4:变量D无缺失(作为对照组)
# ==================== 计算缺失矩阵 ====================
# data_vis.isnull():生成布尔矩阵,True表示缺失
# 这个矩阵用于可视化:缺失的位置显示为红色
missing_matrix = data_vis.isnull()
# ==================== 设置中文字体(避免中文显示乱码) ====================
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备 # 设置黑体字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# ==================== 创建画布和子图 ====================
# figsize=(14, 6):画布宽度14英寸,高度6英寸
# 1, 2:创建1行2列的子图布局(共2个图)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# ==================== 子图1:缺失模式热力图 ====================
# imshow():将矩阵以图像形式显示(热力图)
# missing_matrix.T:转置矩阵,使变量名在Y轴,时间在X轴
# aspect='auto':自动调整纵横比
# cmap='Reds':使用红色渐变色(白色=不缺失,红色=缺失)
# interpolation='none':不插值,保持原始像素
axes[0].imshow(missing_matrix.T, aspect='auto', cmap='Reds', interpolation='none')
# set_yticks():设置Y轴刻度位置(变量行号)
axes[0].set_yticks(range(len(data_vis.columns)))
# set_yticklabels():设置Y轴刻度标签(变量名)
axes[0].set_yticklabels(data_vis.columns)
axes[0].set_xlabel('观测序号') # X轴标签:样本序号1-100
axes[0].set_title('缺失模式热力图(红色=缺失)') # 图表标题
axes[0].grid(False) # 关闭网格线
# ==================== 子图2:缺失条形图 ====================
# missing_matrix.sum():计算每列的缺失值数量
missing_counts = missing_matrix.sum()
# missing_counts / len(data_vis) * 100:计算每列缺失比例
missing_ratios = missing_counts / len(data_vis) * 100
# bar():绘制条形图
# range(len(data_vis.columns)):X轴位置(0,1,2,3对应A,B,C,D)
# missing_ratios:条形高度(缺失比例)
# color='steelblue':条形颜色(钢蓝色)
# edgecolor='black':条形边框颜色(黑色)
axes[1].bar(range(len(data_vis.columns)), missing_ratios,
color='steelblue', edgecolor='black')
# set_xticks():设置X轴刻度位置
axes[1].set_xticks(range(len(data_vis.columns)))
# set_xticklabels():设置X轴刻度标签(变量名A、B、C、D)
axes[1].set_xticklabels(data_vis.columns)
axes[1].set_ylabel('缺失比例(%)') # Y轴标签
axes[1].set_title('各变量缺失比例') # 图表标题
axes[1].grid(axis='y', alpha=0.3) # 显示Y轴网格线,透明度30%
# ==================== 添加数值标签 ====================
# enumerate(missing_ratios):遍历缺失比例列表,返回(索引, 值)
for i, v in enumerate(missing_ratios):
# text():在图表上添加文本
# i, v+1:文本位置(X=索引,Y=比例+1,略高于条形顶部)
# f'{v:.1f}%':文本内容(比例保留1位小数)
# ha='center':水平居中对齐
# fontsize=10:字体大小10
axes[1].text(i, v + 1, f'{v:.1f}%', ha='center', fontsize=10)
# ==================== 调整布局并显示图表 ====================
# tight_layout():自动调整子图间距,避免标题重叠
plt.tight_layout()
# show():显示图表
plt.show()
# ==================== 打印统计信息 ====================
print('缺失值统计:')
print(missing_counts) # 每列的缺失值数量
print(f'\n缺失比例:')
print(missing_ratios) # 每列的缺失比例金融应用:理解缺失模式有助于判断缺失机制,选择合适的处理策略。例如,如果某只股票的成交量在停牌期间系统性缺失(MAR),则不应简单删除这些观测。
16.4 缺失值处理策略
理论背景:缺失数据的统计理论
在逐项学习处理策略之前,必须先回答”缺失是怎么产生的”。从统计学角度来看,缺失值问题远比”补全空缺”复杂。缺失值的产生机制本身可能携带重要信息,而缺失模式的不同决定了我们应该采用的处理策略。Rubin(1976)提出的缺失数据分类理论是处理这一问题的理论基础:
三种缺失机制(Missing Data Mechanisms):
- 完全随机缺失(MCAR - Missing Completely At Random): 缺失的发生与任何变量(观测或未观测)都无关
- 随机缺失(MAR - Missing At Random): 缺失的发生仅依赖于已观测的数据
- 非随机缺失(MNAR - Missing Not At Random): 缺失的发生与缺失值本身相关
理解缺失机制的重要性:
错误的缺失值处理策略可能导致:
- 有偏估计: 删除MNAR数据会系统性扭曲样本
- 损失信息: 过度删除会减少样本量,降低统计功效
- 虚假结论: 不当填充可能创造不存在的模式
记住这三类机制,再来看第一类处理策略。
16.4.1 策略1删除法(Deletion)
删除法是最简单的处理方式,但需要谨慎使用。
# =============================================================================
# 题目:删除缺失值的不同策略
# =============================================================================
# 本任务演示如何使用dropna()方法删除包含缺失值的行或列
# 删除法简单但风险大,需要理解每种策略的影响
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
# ==================== 创建示例数据 ====================
data = {
# 门店代码列
'Store': ['A', 'B', 'C', 'D', 'E'],
# 销售额列:门店B和E缺失
'Sales': [100, np.nan, 150, 200, np.nan],
# 利润列:门店C缺失
'Profit': [10, 15, np.nan, 20, 25]
}
# 转换为数据框
df = pd.DataFrame(data)
# ==================== 显示原始数据 ====================
print('原始数据:')
print(df)
print()
# ==================== 策略1:删除含任何缺失值的行 ====================
# how='any':只要行中有任何一个缺失值,就删除该行
# 这是dropna()的默认参数
df_drop_any = df.dropna(how='any')
print('删除含任何缺失值的行(how="any"):')
print(df_drop_any)
# 结果解读:只保留行A和D(这两行没有任何缺失值)
# 删除了行B(Sales缺失)、行C(Profit缺失)、行E(Sales缺失)
print()
# ==================== 策略2:只删除全部缺失的行 ====================
# how='all':只有当行中所有值都缺失时,才删除该行
# 这是一种宽松的删除策略,保留部分数据
df_drop_all = df.dropna(how='all')
print('只删除全部缺失的行(how="all"):')
print(df_drop_all)
# 结果解读:所有行都保留(因为没有一行是全部缺失的)
# 这个策略在金融数据中很少使用,因为完全缺失的行很少见
print()
# ==================== 策略3:删除特定列含缺失的行 ====================
# subset=['Sales']:只检查Sales列是否缺失
# 如果Sales列缺失,删除该行;其他列缺失不影响
df_drop_subset = df.dropna(subset=['Sales'])
print('只删除Sales列缺失的行:')
print(df_drop_subset)
# 结果解读:删除了行B和E(Sales列缺失),保留行C(Profit缺失但Sales不缺失)
# 应用场景:核心变量(如价格)缺失时,整行数据无意义
print()
# ==================== 策略4:删除含缺失值的列 ====================
# axis=1:按列操作(默认axis=0是按行操作)
# 删除包含任何缺失值的列
df_drop_cols = df.dropna(axis=1)
print('删除含缺失值的列:')
print(df_drop_cols)
# 结果解读:只保留Store列(Sales和Profit列都有缺失,被删除)
# 应用场景:当某列缺失率太高(如>30%),考虑删除该列补充说明:删除法的适用条件
删除法的使用必须满足以下条件:
- MCAR假设: 数据缺失是完全随机的
- 样本量充足: 删除后仍有足够的观测进行统计推断
- 缺失比例低: 通常建议缺失率 < 5%
金融场景注意事项:
在金融时间序列中,直接删除可能导致:
- 时间不连续: 破坏了时间序列的完整性
- 幸存者偏差: 只保留”存活”的数据,高估表现
- 低估风险: 删除极端情况(往往伴随缺失)会低估风险
16.4.2 策略2填充法(Imputation)
填充法用估计值替代缺失值,保留了所有观测。
# =============================================================================
# 题目:多种填充策略对比
# =============================================================================
# 本任务演示6种常见的缺失值填充方法,并比较它们的优劣
# 填充法保留了所有观测,但可能引入偏差
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
# ==================== 创建金融时间序列示例 ====================
# 创建日期序列:2024年1月1日开始,共10天
dates = pd.date_range('2024-01-01', periods=10)
# 价格序列:包含3个缺失值(第2、5、9天)
prices = [100.5, np.nan, 101.2, 100.8, np.nan, np.nan, 102.5, 103.0, np.nan, 104.2]
# 创建数据框
df_prices = pd.DataFrame({'Date': dates, 'Price': prices})
# ==================== 显示原始数据 ====================
print('原始价格数据:')
print(df_prices)
print()
# ==================== 策略1:零填充 ====================
# fillna(0):将所有缺失值填充为0
# 警告:这通常是不合适的,因为价格不可能是0
df_zero = df_prices.fillna(0)
print('零填充(通常不推荐):')
print(df_zero[['Price']].head())
# 结果解读:第2、5、9天的价格被设为0,这在金融数据中没有意义
# 零填充适用于:计数类数据(如交易次数)
print()
# ==================== 策略2:均值填充 ====================
# df_prices['Price'].mean():计算价格序列的均值(忽略NaN)
mean_price = df_prices['Price'].mean()
# fillna(mean_price):将所有缺失值填充为均值
df_mean = df_prices.fillna(mean_price)
print(f'均值填充(均值={mean_price:.2f}):')
print(df_mean[['Price']].head())
# 结果解读:所有缺失位置都填入同一个值(102.03)
# 优点:保持数据的中心趋势
# 缺点:低估方差,破坏时间序列的自相关性
print()
# ==================== 策略3:中位数填充(更稳健) ====================
# df_prices['Price'].median():计算价格序列的中位数
# 中位数比均值更稳健,不受极端值影响
median_price = df_prices['Price'].median()
# fillna(median_price):将所有缺失值填充为中位数
df_median = df_prices.fillna(median_price)
print(f'中位数填充(中位数={median_price:.2f}):')
print(df_median[['Price']].head())
# 优点:对异常值不敏感
# 适用场景:存在极端价格(如涨跌停)时
print()
# ==================== 策略4:前向填充(适合时间序列) ====================
# .ffill():forward fill,用前一个有效值填充(旧版pandas写作 fillna(method='ffill'))
# 这是时间序列数据的常用方法
df_ffill = df_prices.ffill()
print('前向填充(用前一个值填充):')
print(df_ffill[['Price']])
# 结果解读:
# - 第2天:用第1天的100.5填充
# - 第5天:用第4天的100.8填充
# - 第6天:用第5天(已填充)的100.8填充
# - 第9天:用第8天的103.0填充
# 金融应用:填充停牌期间的价格(假设价格保持不变)
print()
# ==================== 策略5:后向填充 ====================
# .bfill():backward fill,用后一个有效值填充(旧版pandas写作 fillna(method='bfill'))
# 这在金融数据中较少使用
df_bfill = df_prices.bfill()
print('后向填充(用后一个值填充):')
print(df_bfill[['Price']])
# 结果解读:
# - 第2天:用第3天的101.2填充
# - 第5天:用第7天的102.5填充
# - 第6天:用第7天的102.5填充
# - 第9天:用第10天的104.2填充
# 问题:使用了"未来"的信息,可能导致前视偏差
print()
# ==================== 策略6:线性插值 ====================
# method='linear':线性插值,在两个已知点之间连线
# interpolate():Pandas的插值方法,支持多种算法
df_linear = df_prices.interpolate(method='linear')
print('线性插值:')
print(df_linear[['Price']])
# 结果解读:
# - 第2天:在100.5和101.2之间线性插值
# - 第5天:在100.8和102.5之间线性插值
# - 第6天:继续插值
# - 第9天:在103.0和104.2之间线性插值
# 公式:y = y1 + (y2-y1)*(x-x1)/(x2-x1)
# 金融应用:填充高频数据的小缺口(假设价格平滑变化)代码深度解析:
- 均值/中位数填充:
- 假设:缺失值围绕中心趋势分布
- 优点:简单,保持数据分布中心
- 缺点:低估方差,破坏变量间关系
- 金融适用:横截面数据(如多只股票的PE)
- 前向填充(Forward Fill):
- 假设:状态持续(价格保持不变)
- 适用:时间序列数据,如股票停牌
- 风险:可能引入滞后偏差
- 金融场景:填充实停期间的价格
- 线性插值:
- 假设:变化是平滑的
- 适用:高频数据,小幅缺失
- 风险:可能创造不存在的价格
- 金融场景:日内价格的小缺口
理论背景:插值方法的数学基础
线性插值假设两个已知点之间的变化是线性的。对于时间点 \(t_1\) 和 \(t_3\) 之间的缺失值 \(t_2\):
\[ y_{t_2} = y_{t_1} + \frac{t_2 - t_1}{t_3 - t_1} (y_{t_3} - y_{t_1}) \]
这等价于假设价格本身服从(算术)布朗运动时,缺失时刻价格的条件期望。金融中更常用的假设是几何布朗运动,此时价格取对数后才是线性演化,应当先对 \(\log y\) 做线性插值、再取指数还原,而不是直接对价格插值。
16.4.3 策略3高级填充方法
对于复杂的缺失模式,需要更精细的方法。
# =============================================================================
# 题目:高级插值方法与滚动统计填充
# =============================================================================
# 本任务演示更复杂的填充策略,包括时间插值、多项式插值和滚动统计
# 这些方法考虑了数据的时序特征和局部模式
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
# ==================== 创建含缺失值的金融数据 ====================
# 设置随机种子
np.random.seed(42)
# 创建日期序列:50个交易日
dates = pd.date_range('2024-01-01', periods=50)
# 生成随机游走价格序列(模拟股票价格)
# np.random.randn(50).cumsum():随机数的累积和(布朗运动)
prices = 100 + np.random.randn(50).cumsum()
# ==================== 人为引入缺失值 ====================
# prices[10:15]:将第10-14个元素(索引10到14)设为NaN
# 这模拟连续5天的停牌
prices[10:15] = np.nan
# prices[30:32]:将第30-31个元素设为NaN
# 这模拟连续2天的数据缺失
prices[30:32] = np.nan
# prices[45]:将第45个元素设为NaN
# 这模拟单日数据缺失
prices[45] = np.nan
# ==================== 创建数据框 ====================
df = pd.DataFrame({'Date': dates, 'Price': prices})
# set_index():将Date列设为索引(便于时间序列操作)
# inplace=True:直接修改原数据框,不创建副本
df.set_index('Date', inplace=True)
# ==================== 显示原始数据 ====================
print('原始数据(含缺失):')
print(df.head(15)) # 显示前15行,可以看到第10-14天的缺失
print()
# ==================== 策略1:时间插值(考虑日期间隔) ====================
# method='time':基于实际时间间隔进行插值
# 这与linear不同:time考虑了日期之间的实际天数间隔
df_time = df.interpolate(method='time')
print('时间插值(考虑实际日期间隔):')
print(df_time.head(15))
# 适用场景:日期不连续(如跳过周末)
# 优点:更符合实际时间流逝
# 注意:如果日期是连续的,time和linear结果相同
print()
# ==================== 策略2:多项式插值 ====================
# method='polynomial':使用多项式插值
# order=2:二阶多项式(二次函数)
# 这假设价格变化遵循二次曲线
df_poly = df.interpolate(method='polynomial', order=2)
print('多项式插值(二阶):')
print(df_poly.head(15))
# 数学原理:拟合一个二次函数 y = ax² + bx + c
# 优点:可以捕捉非线性趋势
# 缺点:可能在缺失区间边缘产生振荡(龙格现象)
# 金融慎用:价格通常不遵循简单的多项式
print()
# ==================== 策略3:滚动均值填充(利用历史信息) ====================
# rolling(window=5, min_periods=1):计算滚动窗口统计量
# window=5:窗口大小为5(使用前5个数据点)
# min_periods=1:最少需要1个非缺失值(这样窗口起始处也能计算)
# .mean():计算均值
df_rolling = df.fillna(df.rolling(window=5, min_periods=1).mean())
print('滚动均值填充(窗口=5):')
print(df_rolling.head(15))
# 原理:用前5天的平均价格填充缺失值
# 优点:考虑了局部趋势,比全局均值更合理
# 金融应用:填充短期停牌价格(参考近期水平)
# 注意:这里fillna()的参数是一个Series(滚动均值序列)
print()
# ==================== 策略4:分组填充(不同资产分别处理) ====================
# 场景:多只股票的数据,每只股票独立处理
# 创建多只股票的数据
stocks = pd.DataFrame({
# 日期列:10个日期各出现两遍,股票A与股票B分别对应完整的10个交易日
'Date': pd.date_range('2024-01-01', periods=10).tolist() * 2,
# 股票代码列:股票A重复10次,股票B重复10次
'Stock': ['A'] * 10 + ['B'] * 10,
# 价格列:包含多个缺失值
'Price': [100, 101, np.nan, 103, 104, np.nan, 106, 107, np.nan, 109,
50, np.nan, 52, 53, 54, np.nan, np.nan, 58, 59, 60]
})
# ==================== 显示多股票数据 ====================
print('多只股票数据:')
print(stocks)
print()
# ==================== 分组前向填充 ====================
# groupby('Stock'):按股票代码分组
# .ffill():对每组分别前向填充(旧版pandas写作 fillna(method='ffill'))
# 这确保股票A的缺失不会用股票B的数据填充
stocks_filled = stocks.groupby('Stock').ffill()
print('分组前向填充(每只股票独立处理):')
print(stocks_filled)
# 结果解读:
# - 股票A的第3天缺失:用第2天的101填充
# - 股票B的第2天缺失:用第1天的50填充(不是用股票A的数据)
# 金融应用:处理面板数据(多只股票的时间序列)
# 关键:不同股票的数据不能混用补充说明:金融时间序列的特殊性
金融时间序列的特殊性质决定了填充策略:
- 非平稳性: 价格通常不是平稳的,收益率更平稳
- 建议: 对收益率序列进行插值,而非价格
- 波动聚集: 方差随时间变化
- 建议: 使用滚动窗口统计,而非全局统计
- 杠杆效应: 负收益和正收益的影响不对称
- 建议: 分别对正负收益进行插值
- 交易机制: 涨跌停、停牌等导致缺失
- 建议: 保留这些信息,不强制填充
16.5 实战案例英国零售商数据清洗
16.5.1 数据加载与初步诊断
# =============================================================================
# 题目:英国零售商数据加载与质量评估
# =============================================================================
# 本任务演示真实商业数据的加载、诊断和质量评估流程
# 数据来源于英国在线零售商的交易记录
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
# ==================== 注意:实际数据加载(注释掉) ====================
# 在实际使用中,需要提供正确的文件路径
# path = '英国零售商data.csv' # 数据文件路径
# df = pd.read_csv(path, dtype={'CustomerID': str, 'InvoiceID': str})
# dtype参数:指定列的数据类型(防止客户ID被误读为数值)
# ==================== 模拟英国零售商数据(演示用) ====================
# 设置随机种子,确保结果可重现
np.random.seed(42)
# 记录数:1000条交易记录
n_records = 1000
# ==================== 创建数据框 ====================
df_retail = pd.DataFrame({
# 发票ID列:生成1000个唯一的发票编号
# f'INV{i:06d}':格式化字符串,如INV000001, INV000002
'InvoiceID': [f'INV{i:06d}' for i in range(n_records)],
# 客户ID列:10%的记录缺失客户ID
# np.random.randint(1, 201):随机生成1-200的客户ID
# np.random.rand() > 0.1:90%的概率有客户ID,10%概率为NaN
'CustomerID': [f'CUST{np.random.randint(1, 201):05d}' if np.random.rand() > 0.1 else np.nan
for _ in range(n_records)],
# 交易日期列:从2023-01-01开始的1000个连续日期
'InvoiceDate': pd.date_range('2023-01-01', periods=n_records),
# 商品数量列:随机生成1-100之间的整数
'Quantity': np.random.randint(1, 100, n_records),
# 单价列:随机生成1-50之间的浮点数(英镑)
'UnitPrice': np.random.uniform(1, 50, n_records),
# 国家列:所有记录都是英国(UK)
'Country': ['UK'] * n_records
})
# ==================== 人为引入缺失值(模拟真实场景) ====================
# 随机选择50个位置,将CustomerID设为NaN
df_retail.loc[np.random.choice(n_records, 50), 'CustomerID'] = np.nan
# 随机选择30个位置,将Quantity设为NaN
df_retail.loc[np.random.choice(n_records, 30), 'Quantity'] = np.nan
# 随机选择20个位置,将UnitPrice设为NaN
df_retail.loc[np.random.choice(n_records, 20), 'UnitPrice'] = np.nan
# ==================== 数据基本信息 ====================
print('数据形状:', df_retail.shape)
# .shape返回(行数, 列数),输出(1000, 6)表示1000行6列
print('\n数据类型:')
# .dtypes:显示每列的数据类型
print(df_retail.dtypes)
print('\n前10行数据:')
# .head(10):显示前10行数据,快速了解数据结构
print(df_retail.head(10))
# ==================== 缺失值诊断 ====================
print('\n缺失值统计:')
# .isnull().sum():统计每列的缺失值数量
print(df_retail.isnull().sum())
print('\n缺失值比例:')
# .isnull().mean() * 100:计算每列缺失值百分比
print(df_retail.isnull().mean() * 100)16.5.2 缺失值处理决策
# =============================================================================
# 题目:零售商数据缺失值处理流程
# =============================================================================
# 本任务演示一个完整的缺失值处理决策流程
# 关键:根据业务逻辑选择不同的处理策略
# ==================== 复制数据用于处理 ====================
# .copy():创建数据框的副本,避免修改原始数据
# 好习惯:保留原始数据,以便对比处理前后的差异
df_clean = df_retail.copy()
print('=== 缺失值处理流程 ===\n')
# ==================== 步骤1:处理CustomerID缺失 ====================
# 决策:删除CustomerID缺失的行
# 理由:客户ID是关联交易的关键字段,无法合理推测
# 业务逻辑:没有客户ID的交易无法进行客户行为分析
before_customer = len(df_clean) # 处理前的行数
# dropna(subset=['CustomerID']):删除CustomerID列缺失的行
# subset参数:只检查指定列的缺失值
# inplace=True:直接修改df_clean(不需要重新赋值)
df_clean.dropna(subset=['CustomerID'], inplace=True)
after_customer = len(df_clean) # 处理后的行数
print(f'1. CustomerID处理:')
print(f' - 删除前: {before_customer} 行')
print(f' - 删除后: {after_customer} 行')
# 计算删除比例:(处理前行数 - 处理后行数) / 处理前行数
print(f' - 删除比例: {(before_customer - after_customer) / before_customer:.2%}')
# .2%:格式化为百分比,保留2位小数
print()
# ==================== 步骤2:处理Quantity缺失 ====================
# 决策:用中位数填充
# 理由:数量是数值型变量,中位数稳健,不受异常值影响
# 业务逻辑:用"典型订单量"替代缺失值
median_quantity = df_clean['Quantity'].median()
# .median():计算中位数(50%分位数)
# fillna(median_quantity):将Quantity列的NaN填充为中位数(使用赋值形式,兼容Pandas 2.x)
df_clean['Quantity'] = df_clean['Quantity'].fillna(median_quantity)
print(f'2. Quantity处理:')
print(f' - 填充值(中位数): {median_quantity}')
# .isnull().sum():检查剩余缺失值数量(应该为0)
print(f' - 剩余缺失: {df_clean["Quantity"].isnull().sum()}')
print()
# ==================== 步骤3:处理UnitPrice缺失 ====================
# 决策:按CustomerID分组填充均值
# 理由:同一客户的消费水平可能相似(价格敏感性一致)
# 业务逻辑:老客户通常购买相似价位的商品
# groupby('CustomerID'):按客户ID分组
# ['UnitPrice']:选择UnitPrice列
# .transform(lambda x: x.fillna(x.mean())):对每组应用转换函数
# lambda x:匿名函数,x是每个客户的UnitPrice序列
# x.fillna(x.mean()):用该客户的平均价格填充缺失值
df_clean['UnitPrice'] = df_clean.groupby('CustomerID')['UnitPrice'].transform(
lambda x: x.fillna(x.mean())
)
# 如果仍有缺失(某客户所有记录的UnitPrice都缺失),用全局均值填充(赋值形式兼容Pandas 2.x)
df_clean['UnitPrice'] = df_clean['UnitPrice'].fillna(df_clean['UnitPrice'].mean())
print(f'3. UnitPrice处理:')
print(f' - 分组均值填充')
print(f' - 剩余缺失: {df_clean["UnitPrice"].isnull().sum()}')
print()
# ==================== 最终验证 ====================
print('=== 处理后数据质量 ===')
print(f'总行数: {len(df_clean)}')
# df_clean.dropna():删除仍有缺失的行(应该没有)
print(f'完整行数: {df_clean.dropna().shape[0]}')
print(f'\n最终缺失值统计:')
print(df_clean.isnull().sum())
# 期望输出:所有列的缺失值都是0代码深度解析:
- 删除CustomerID缺失行:
- 业务逻辑:CustomerID是客户分析的关键,无法推测
- 删除合理性:假设缺失是完全随机的(数据录入错误)
- 影响评估:删除比例控制在可接受范围
- Quantity中位数填充:
- 选择中位数原因:数量可能有极端值(大额批发)
- 中位数稳健性:不受异常值影响
- 业务合理性:用”典型订单量”替代缺失
- UnitPrice分组填充:
- 分组逻辑:同一客户购买相似商品,价格水平接近
- 两步策略:先分组填充,再全局填充
- 优势:保留客户特定的价格信息
16.5.3 数据质量报告
# =============================================================================
# 题目:生成数据质量报告
# =============================================================================
# 本任务演示如何生成一份全面的数据质量报告
# 报告包含基本统计、数据分布、业务指标等
# ==================== 打印报告标题 ====================
print('=== 数据质量报告 ===\n')
# ==================== 1. 基本统计信息 ====================
print('1. 数据概览:')
# len(df_clean):总记录数(行数)
print(f' - 总记录数: {len(df_clean)}')
# .nunique():唯一值数量(有多少个不同客户)
print(f' - 唯一客户数: {df_clean["CustomerID"].nunique()}')
# .min()和.max():时间范围(从哪天到哪天)
print(f' - 时间范围: {df_clean["InvoiceDate"].min()} 至 {df_clean["InvoiceDate"].max()}')
print()
# ==================== 2. 数值变量统计 ====================
print('2. 数值变量统计:')
# [['Quantity', 'UnitPrice']]:选择这两列
# .describe():计算描述性统计量(计数、均值、标准差、最小值、分位数、最大值)
print(df_clean[['Quantity', 'UnitPrice']].describe())
# 输出解读:
# - count:非缺失值数量
# - mean:平均值
# - std:标准差(衡量波动性)
# - min:最小值
# - 25%:第一四分位数(25%分位)
# - 50%:中位数
# - 75%:第三四分位数(75%分位)
# - max:最大值
print()
# ==================== 3. 计算总金额 ====================
# Quantity * UnitPrice:计算每笔交易的金额
# 这创建了一个新列TotalAmount
df_clean['TotalAmount'] = df_clean['Quantity'] * df_clean['UnitPrice']
print('3. 交易金额统计:')
# .sum():总交易额(所有交易金额之和)
print(f' - 总交易额: £{df_clean["TotalAmount"].sum():,.2f}')
# :,.2f:格式化为千分位,保留2位小数,如£1,234,567.89
# .mean():平均交易额
print(f' - 平均交易额: £{df_clean["TotalAmount"].mean():.2f}')
# .max():最大交易额
print(f' - 最大交易额: £{df_clean["TotalAmount"].max():.2f}')
print()
# ==================== 4. 客户分析 ====================
# groupby('CustomerID'):按客户ID分组
# .agg({...}):对每列应用不同的聚合函数
customer_stats = df_clean.groupby('CustomerID').agg({
'TotalAmount': 'sum', # 总金额:求和
'InvoiceID': 'count' # 交易次数:计数
}).rename(columns={'InvoiceID': 'TransactionCount'}) # 重命名列(InvoiceID改为TransactionCount)
print('4. 客户统计(Top 5):')
# .nlargest(5, 'TotalAmount'):选择TotalAmount最大的5个客户
print(customer_stats.nlargest(5, 'TotalAmount'))
# 应用场景:识别VIP客户,进行精准营销16.6 金融应用案例股票停牌数据处理
16.6.1 场景描述
中国A股市场的停牌机制导致价格序列出现缺失。正确处理这些缺失值对计算收益率、波动率等指标至关重要。
金融场景中的缺失值:
停牌只是金融数据缺失的一个典型来源,完整的来源清单还包括:
- 停牌交易日: 股票停牌期间无价格和成交量数据
- 新上市公司: 缺乏历史价格数据
- 财务数据: 部分公司延迟发布财报
- 数据源错误: API采集失败、存储故障
- 数据结构变化: 指数成分股调整导致历史数据不连续
# =============================================================================
# 题目:股票停牌数据处理
# =============================================================================
# 本任务演示如何处理股票停牌期间的价格缺失
# 停牌是中国A股市场的特殊机制,可能导致连续多日无交易数据
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库
import matplotlib.pyplot as plt # Matplotlib绘图库
# ==================== 设置中文字体 ====================
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备 # 设置黑体字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# ==================== 模拟股票价格数据(含停牌) ====================
# 创建日期序列:30个交易日
dates = pd.date_range('2024-01-01', periods=30)
# 设置随机种子,确保结果可重现
np.random.seed(42)
# 生成正常价格序列:随机游走
# np.random.randn(30).cumsum():30个随机数的累积和
# * 0.5:调整波动幅度
prices_normal = 100 + np.random.randn(30).cumsum() * 0.5
# ==================== 人为创建停牌(价格缺失) ====================
# .copy():复制正常价格序列
prices_suspended = prices_normal.copy()
# 第10-14行(索引10:15)设为NaN:模拟第一次停牌5天
prices_suspended[10:15] = np.nan
# 第22-23行(索引22:24)设为NaN:模拟第二次停牌2天
prices_suspended[22:24] = np.nan
# ==================== 创建数据框 ====================
df_stock = pd.DataFrame({
'Date': dates,
'Price': prices_suspended, # 含停牌的价格序列
'Price_Normal': prices_normal # 真实价格序列(用于对比)
})
# ==================== 显示数据 ====================
print('股票价格数据(含停牌):')
print(df_stock.head(15)) # 显示前15行,可以看到第一次停牌
print()
print('缺失值统计:')
# .isnull().sum():统计Price列的缺失值数量
print(f'缺失天数: {df_stock["Price"].isnull().sum()}')
# .isnull().mean():计算缺失比例
print(f'缺失比例: {df_stock["Price"].isnull().mean():.2%}')
print()
# ==================== 不同处理策略对比 ====================
# 策略1:前向填充(用前一个有效值填充)
df_stock['ForwardFill'] = df_stock['Price'].ffill()
# 策略2:线性插值(在两个已知点之间连线)
df_stock['LinearInterp'] = df_stock['Price'].interpolate(method='linear')
# 策略3:零填充(通常不推荐)
df_stock['ZeroFill'] = df_stock['Price'].fillna(0)
# ==================== 计算日收益率 ====================
# .pct_change():计算百分比变化(收益率)
# 公式:(当前值 - 前一个值) / 前一个值
df_stock['Return_Normal'] = df_stock['Price_Normal'].pct_change()
df_stock['Return_FF'] = df_stock['ForwardFill'].pct_change()
df_stock['Return_Lin'] = df_stock['LinearInterp'].pct_change()
# ==================== 可视化对比 ====================
# 创建2行1列的子图布局
fig, axes = plt.subplots(2, 1, figsize=(14, 10))
# ==================== 子图1:价格对比 ====================
# 绘制真实价格(绿色实线)
axes[0].plot(df_stock['Date'], df_stock['Price_Normal'], 'g-', label='真实价格(无缺失)', linewidth=2)
# 绘制前向填充价格(蓝色虚线)
axes[0].plot(df_stock['Date'], df_stock['ForwardFill'], 'b--', label='前向填充', linewidth=1.5)
# 绘制线性插值价格(红色虚线,透明度70%)
axes[0].plot(df_stock['Date'], df_stock['LinearInterp'], 'r--', label='线性插值', linewidth=1.5, alpha=0.7)
# 标记停牌期间的真实价格(红色散点)
# df_stock[df_stock['Price'].isnull()]:选择Price为NaN的行
axes[0].scatter(df_stock[df_stock['Price'].isnull()]['Date'],
df_stock['Price_Normal'][df_stock['Price'].isnull()],
color='red', s=50, zorder=5, label='停牌期间真实价格')
axes[0].set_ylabel('价格', fontsize=12)
axes[0].set_title('股票停牌数据处理:价格对比', fontsize=14)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)
# ==================== 子图2:收益率对比 ====================
# 绘制真实收益率(绿色实线)
axes[1].plot(df_stock['Date'], df_stock['Return_Normal'], 'g-', label='真实收益率', linewidth=2)
# 绘制前向填充收益率(蓝色虚线)
axes[1].plot(df_stock['Date'], df_stock['Return_FF'], 'b--', label='前向填充收益率', linewidth=1.5)
# 绘制线性插值收益率(红色虚线)
axes[1].plot(df_stock['Date'], df_stock['Return_Lin'], 'r--', label='线性插值收益率', linewidth=1.5, alpha=0.7)
# 添加零线(黑色细线)
axes[1].axhline(y=0, color='k', linestyle='-', linewidth=0.5)
axes[1].set_xlabel('日期', fontsize=12)
axes[1].set_ylabel('日收益率', fontsize=12)
axes[1].set_title('股票停牌数据处理:收益率对比', fontsize=14)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)
# ==================== 调整布局并显示 ====================
plt.tight_layout()
plt.show()
# ==================== 误差分析 ====================
# 计算均方误差(MSE):预测值与真实值偏差的平方的平均
# ((ForwardFill - Price_Normal) ** 2):计算偏差的平方
# .mean():求平均
mse_ff = ((df_stock['ForwardFill'] - df_stock['Price_Normal']) ** 2).mean()
mse_lin = ((df_stock['LinearInterp'] - df_stock['Price_Normal']) ** 2).mean()
print('填充误差评估:')
print(f'前向填充MSE: {mse_ff:.4f}')
print(f'线性插值MSE: {mse_lin:.4f}')
# MSE越小,填充效果越好
# 通常前向填充的MSE更小(因为停牌期间价格往往保持不变)金融应用要点:
- 前向填充(Forward Fill):
- 适用场景:停牌期间价格保持不变的假设
- 优势:保持价格水平,收益率在复牌时反映真实变化
- 风险:复牌时的收益率包含了停牌期间的全部变化
- 线性插值:
- 适用场景:假设停牌期间价格持续平滑变化
- 优势:分散变化到整个停牌期
- 风险:可能创造不存在的中间价格
- 最佳实践:
- 对于价格序列:使用前向填充
- 对于收益率序列:直接设为0或NaN(停牌无收益)
- 保留”停牌”标识,避免计算统计量时误入
16.7 本章小结
要点:
- 缺失诊断三板斧:
isnull().sum()计数、isnull().mean()比例、dropna().shape[0]完整行数 - 删除法看
how='any'/'all'与subset;填充法看常数、均值/中位数、ffill/bfill、interpolate - 时间序列优先前向填充或插值;面板数据先
groupby再分组填充,避免跨个体”串数据” - 处理策略必须回到缺失机制(MCAR/MAR/MNAR)与业务含义来论证
易错点:
- 对价格序列盲目均值填充会低估波动、破坏自相关结构
bfill使用了未来信息,在回测中会造成前视偏差- 对价格直接线性插值隐含算术布朗运动假设;几何布朗运动下应先取对数再插值
16.8 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import pandas as pd import numpy as np df = pd.DataFrame({'id': ['a', 'b', None, 'd'], 'v': [1, np.nan, 3, np.nan]}) print(df.isnull().sum()) print(df.dropna().shape) print(df.dropna(subset=['id']).shape) print(df['v'].fillna(df['v'].mean()))参考答案(先写下你的预测再点开)
解题思路:逐行推演。
df.isnull().sum()逐列数缺失:id列的None记 1 个(对象列中None视为缺失),v列的np.nan有两个,记 2 个。df.dropna()默认how='any',任一列缺失即删行——第 1 行(id=‘a’,v=1)是唯一完整行,shape为(1, 2)。df.dropna(subset=['id'])只按id列判断:第 3 行的 id 是None被删,第 2、4 行虽然 v 缺失但保留,shape为(3, 2)。df['v'].fillna(df['v'].mean()):v 的非缺失均值是 (1+3)/2=2.0,两个缺失位置都填 2.0,输出 [1.0, 2.0, 3.0, 2.0];注意fillna返回新 Series,原df不变。# 验证脚本:逐行输出四个表达式的求值结果 import numpy as np # 导入NumPy库,用于生成NaN import pandas as pd # 导入Pandas库 df = pd.DataFrame({'id': ['a', 'b', None, 'd'], 'v': [1, np.nan, 3, np.nan]}) # 4行2列,id与v各含缺失 print(df.isnull().sum()) # 逐列统计缺失值数量 print(df.dropna().shape) # 默认how='any':任一列缺失即删行 print(df.dropna(subset=['id']).shape) # 只按id列判断:删除后剩3行 print(df['v'].fillna(df['v'].mean())) # 用v列非缺失均值2.0填充缺失位置预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
id 1 v 2 dtype: int64 (1, 2) (3, 2) 0 1.0 1 2.0 2 3.0 3 2.0 Name: v, dtype: float64回扣本章:对应本章小结“要点”第 1 条——缺失诊断三板斧:
isnull().sum()计数、按subset限定删除口径、fillna以列统计量填充。概念辨析:
dropna(how='any')、dropna(how='all')、dropna(subset=[...])的删除行为有何不同?ffill与bfill各自引入什么偏差?参考答案(点开前请先独立完成)
解题思路:逐点作答。第一,三种删除口径:
how='any'最严格——行内任一列缺失即删整行,适合“后续分析要求每行字段齐全”的场景,代价是样本损失最大;how='all'最宽松——只有整行全缺才删,主要用于剔除Excel导出时常见的全空行(如 列表 17.1 中先用dropna(how='all')清掉空行的做法,参见第 章节 17 章);subset=[...]折中——只看指定列的缺失,其他列不影响行去留,适合“核心变量缺失才整行无意义”的场景,如平台任务只按CustomerID剔除(列表 16.1)。第二,两种填充的偏差:ffill用前一个有效值填充,隐含“状态持续”(价格停在缺口前水平)的假设,只用历史信息,风险是把缺口期的变化全部推迟到缺口后集中释放;bfill用后一个有效值填充,使用了未来信息,在回测与预测场景造成前视偏差,一般只用于纯历史描述性的补全,不用于任何会“向前预测”的流程。回扣本章:对应本章小结“要点”第 2、4 条与“易错点”第 2 条——删除看
how与subset,填充策略必须回到缺失机制与业务含义论证;bfill会造成前视偏差。变式任务:对本章 10 日价格缺失序列,把
ffill、bfill、interpolate三种填充结果并排拼成一张宽表,逐个缺口比较三者假设的差异。参考答案(点开前请先独立完成)
解题思路:序列(同 列表 16.5)共有三处缺口:01-02 单日缺口、01-05 至 01-06 双日缺口、01-09 单日缺口。把三种填充结果作为新列并到同一张宽表逐缺口对照。以单日缺口 01-02 为例:
ffill填前值 100.5(假设价格停在缺口前水平);bfill填后值 101.2(假设从缺口后水平倒推);interpolate取两邻点均值 100.85=(100.5+101.2)/2(假设价格平滑变化)。双日缺口 01-05 至 01-06 更能区分假设:插值把 100.8 到 102.5 的总变化按时间均分到两日(101.366667、101.933333),ffill两日都填 100.8,bfill两日都填 102.5。三种方法没有绝对优劣,取决于对缺口期价格行为的假设;对金融价格序列,bfill使用未来信息的风险最大。# 变式程序:三种填充结果并排成宽表逐缺口比较 import numpy as np # 导入NumPy库,用于生成NaN import pandas as pd # 导入Pandas库 price_sequence = [100.5, np.nan, 101.2, 100.8, np.nan, np.nan, 102.5, 103.0, np.nan, 104.2] # 本章10日价格缺失序列 gap_table = pd.DataFrame({'原始': price_sequence}, index=pd.date_range('2024-01-01', periods=10)) # 以日期为行索引的宽表骨架 gap_table['ffill'] = gap_table['原始'].ffill() # 前向填充:缺口填前一个有效值 gap_table['bfill'] = gap_table['原始'].bfill() # 后向填充:缺口填后一个有效值 gap_table['interpolate'] = gap_table['原始'].interpolate(method='linear') # 线性插值:相邻两个已知点连线取值 print(gap_table) # 三种填充结果并排比较预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
原始 ffill bfill interpolate 2024-01-01 100.5 100.5 100.5 100.500000 2024-01-02 NaN 100.5 101.2 100.850000 2024-01-03 101.2 101.2 101.2 101.200000 2024-01-04 100.8 100.8 100.8 100.800000 2024-01-05 NaN 100.8 102.5 101.366667 2024-01-06 NaN 100.8 102.5 101.933333 2024-01-07 102.5 102.5 102.5 102.500000 2024-01-08 103.0 103.0 103.0 103.000000 2024-01-09 NaN 103.0 104.2 103.600000 2024-01-10 104.2 104.2 104.2 104.200000注意:以上为本题变式的独立代码;平台任务 列表 16.1 对应的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 2、3 条——时间序列优先前向填充或插值,插值隐含“平滑变化”假设,而
bfill引入未来信息。动手验证:构造两只有缺失值的价格序列(长格式,含”股票”标识列),分别用整体
ffill与groupby('股票').ffill()填充,找出被跨股票污染的填充值。参考答案(点开前请先独立完成)
解题思路:构造长格式数据,股票 A 的 5 天数据排在前、股票 B 的 5 天排在后,并让 B 的第 1 天缺失。整体
ffill不区分股票、沿着整列顺序前向填充——B 第 1 天的缺失会被A 最后一天的价格 12.0 填上,这就是跨股票污染;groupby('股票')['收盘价'].ffill()在每只股票内部独立前向填充——B 第 1 天前面没有 B 的任何有效值,保持NaN不变。定位污染行的判据:分组填充后仍缺失、而整体填充已补上值的行。# 验证脚本:对比整体ffill与分组ffill,定位被污染的填充值 import numpy as np # 导入NumPy库,用于生成NaN import pandas as pd # 导入Pandas库 stocks_long = pd.DataFrame({ # 长格式两只股票各5日,股票B第1日缺失 '日期': pd.date_range('2024-01-01', periods=5).tolist() * 2, '股票': ['A'] * 5 + ['B'] * 5, '收盘价': [10.0, 10.5, 11.0, 11.5, 12.0, np.nan, 20.0, 20.5, 21.0, 21.5] }) stocks_long['整体ffill'] = stocks_long['收盘价'].ffill() # 整体前向填充:不区分股票 stocks_long['分组ffill'] = stocks_long.groupby('股票')['收盘价'].ffill() # 分组前向填充:每只股票独立 print(stocks_long) # 查看两种填充的完整结果 print(stocks_long.loc[stocks_long['分组ffill'].isna() & stocks_long['整体ffill'].notna()]) # 分组填充仍缺失而整体填充已补上的行,即被跨股票污染的行预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
日期 股票 收盘价 整体ffill 分组ffill 0 2024-01-01 A 10.0 10.0 10.0 1 2024-01-02 A 10.5 10.5 10.5 2 2024-01-03 A 11.0 11.0 11.0 3 2024-01-04 A 11.5 11.5 11.5 4 2024-01-05 A 12.0 12.0 12.0 5 2024-01-01 B NaN 12.0 NaN 6 2024-01-02 B 20.0 20.0 20.0 7 2024-01-03 B 20.5 20.5 20.5 8 2024-01-04 B 21.0 21.0 21.0 9 2024-01-05 B 21.5 21.5 21.5 日期 股票 收盘价 整体ffill 分组ffill 5 2024-01-01 B NaN 12.0 NaN回扣本章:对应本章小结“要点”第 3 条——面板数据先
groupby再分组填充,避免跨个体“串数据”;上例中 B 的首日被 A 的末日 12.0 污染。